package com.ideaaedi.springcloud.jd.user.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.google.common.collect.Sets;
import com.ideaaedi.springcloud.jd.commonds.constant.BaseConstant;
import com.ideaaedi.springcloud.jd.commonds.constant.JdSymbolConstant;
import com.ideaaedi.springcloud.jd.commonds.entity.BaseCodeMsgEnum;
import com.ideaaedi.springcloud.jd.commonds.entity.PageInfo;
import com.ideaaedi.springcloud.jd.commonds.event.user.SysMenuBatchDeletedEvent;
import com.ideaaedi.springcloud.jd.commonds.event.user.SysMenuDeletedEvent;
import com.ideaaedi.springcloud.jd.commonds.event.user.SysMenuUpdatedEvent;
import com.ideaaedi.springcloud.jd.commonds.entity.user.po.SysMenuPO;
import com.ideaaedi.springcloud.jd.commonds.entity.user.po.SysRoleMenuRePO;
import com.ideaaedi.springcloud.jd.commonds.entity.user.vo.req.SysMenuAddReqVO;
import com.ideaaedi.springcloud.jd.commonds.entity.user.vo.req.SysMenuBatchAddItemReqVO;
import com.ideaaedi.springcloud.jd.commonds.entity.user.vo.req.SysMenuBatchAddMainReqVO;
import com.ideaaedi.springcloud.jd.commonds.entity.user.vo.req.SysMenuBatchAddReqVO;
import com.ideaaedi.springcloud.jd.commonds.entity.user.vo.req.SysMenuListAllVO;
import com.ideaaedi.springcloud.jd.commonds.entity.user.vo.req.SysMenuListReqVO;
import com.ideaaedi.springcloud.jd.commonds.entity.user.vo.req.SysMenuUpdateReqVO;
import com.ideaaedi.springcloud.jd.commonds.entity.user.vo.resp.SysMenuDetailRespVO;
import com.ideaaedi.springcloud.jd.commonds.entity.user.vo.resp.SysMenuListRespVO;
import com.ideaaedi.springcloud.jd.commonds.exception.BaseException;
import com.ideaaedi.springcloud.jd.commonds.util.CommonUtil;
import com.ideaaedi.springcloud.jd.commonspring.config.properties.TenantProperties;
import com.ideaaedi.springcloud.jd.commonspring.mybatisplus.ServiceImplExt;
import com.ideaaedi.springcloud.jd.commonspring.mybatisplus.tenant.TenantProvider;
import com.ideaaedi.springcloud.jd.commonspring.util.JdContextUtil;
import com.ideaaedi.springcloud.jd.commonspring.validator.SysValidator;
import com.ideaaedi.springcloud.jd.commonspring.validator.ValidId;
import com.ideaaedi.springcloud.jd.commonspring.validator.ValidTenant;
import com.ideaaedi.springcloud.jd.user.mapper.SysMenuMapper;
import com.ideaaedi.springcloud.jd.user.service.SysMenuService;
import com.ideaaedi.springcloud.jd.user.service.SysRoleMenuReService;
import com.ideaaedi.springcloud.jd.user.service.SysRoleService;
import com.ideaaedi.springcloud.jd.user.service.SysTenantService;
import com.ideaaedi.springcloud.jd.user.support.UserCheckSupport;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.BeanUtils;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * <p>
 * 系统-菜单表（资源组表） 服务实现类
 * </p>
 *
 * @author <font size = "20" color = "#3CAA3C"><a href="https://gitee.com/JustryDeng">JustryDeng</a></font> <img
 * src="https://gitee.com/JustryDeng/shared-files/raw/master/JustryDeng/avatar.jpg" />
 * @since 2021.0.1.A
 */
@Slf4j
@Service
public class SysMenuServiceImpl extends ServiceImplExt<SysMenuMapper, SysMenuPO> implements SysMenuService {
    
    @Resource
    private TenantProperties tenantProperties;
    
    @Resource
    private SysRoleMenuReService sysRoleMenuReService;
    
    @Resource
    private SysMenuService sysMenuService;
    
    @Resource
    private SysRoleService sysRoleService;
    
    @Resource
    private SysTenantService sysTenantService;
    
    @Resource
    private UserCheckSupport userCheckSupport;
    
    private final AntPathMatcher antPathMatcher = new AntPathMatcher();
    
    @Override
    public PageInfo<SysMenuListRespVO> list(SysMenuListReqVO req) {
        final Long userId = req.getUserId();
        Objects.requireNonNull(userId, "userId cannot be null.");
        Set<Long> roleIdSet = sysRoleService.loadCurrUserRoleIds(userId);
        if (CollectionUtils.isEmpty(roleIdSet)) {
            // 无任何角色，直接返回即可
            log.info("no any available role.");
            return PageInfo.of(0, req.getPageNum(), req.getPageSize(), Collections.emptyList());
        }
        
        Set<Long> hasPermissionMenuIdSet = loadMenuIdByRoleIds(roleIdSet);
        if (CollectionUtils.isEmpty(hasPermissionMenuIdSet)) {
            // 无任何菜单，直接返回即可
            log.info("has no any menu permission.");
            return PageInfo.of(0, req.getPageNum(), req.getPageSize(), Collections.emptyList());
        }
        
        // 正常筛选
        String name = req.getName();
        Long pid = req.getPid();
        String code = req.getCode();
        Integer type = req.getType();
        String icon = req.getIcon();
        String component = req.getComponent();
        String componentName = req.getComponentName();
        Integer visible = req.getVisible();
        String remark = req.getRemark();
        String tenant = req.getTenant();
        IPage<SysMenuPO> pageInfo = new Page<>(req.getPageNum(), req.getPageSize());
        IPage<SysMenuPO> page = this.page(pageInfo, new LambdaQueryWrapper<SysMenuPO>()
                .eq(pid != null, SysMenuPO::getPid, pid)
                .eq(type != null, SysMenuPO::getType, type)
                .eq(visible != null, SysMenuPO::getVisible, visible)
                .likeRight(StringUtils.isNotBlank(tenant), SysMenuPO::getTenant, tenant)
                .like(StringUtils.isNotBlank(code), SysMenuPO::getCode, code)
                .like(StringUtils.isNotBlank(name), SysMenuPO::getName, name)
                .like(StringUtils.isNotBlank(remark), SysMenuPO::getRemark, remark)
                .like(StringUtils.isNotBlank(icon), SysMenuPO::getIcon, icon)
                .like(StringUtils.isNotBlank(component), SysMenuPO::getComponent, component)
                .like(StringUtils.isNotBlank(componentName), SysMenuPO::getComponentName, componentName)
                .in(SysMenuPO::getId, hasPermissionMenuIdSet)
                .last("ORDER BY IFNULL(sort, " + Integer.MAX_VALUE + ") ASC, id DESC")
        );
        
        // 查询租户信息
        List<SysMenuPO> records = page.getRecords();
        Set<String> tenantSet = records.stream()
                .filter(Objects::nonNull)
                .map(SysMenuPO::getTenant).collect(Collectors.toSet());
        Map<String, String> tenantAndNameMap = sysTenantService.mappingTenantName(tenantSet, true);
        
        // 组装数据
        List<SysMenuListRespVO> list = page.getRecords().stream().map(po -> {
            SysMenuListRespVO resp = new SysMenuListRespVO();
            BeanUtils.copyProperties(po, resp);
            resp.setTenantName(tenantAndNameMap.get(resp.getTenant()));
            return resp;
        }).collect(Collectors.toList());
        return PageInfo.of(page.getTotal(), req.getPageNum(), req.getPageSize(), list);
    }
    
    @Override
    public PageInfo<SysMenuListRespVO> listAll(SysMenuListAllVO req) {
        String code = req.getCode();
        Long pid = req.getPid();
        String name = req.getName();
        String tenant = req.getTenant();
    
        IPage<SysMenuPO> pageInfo = new Page<>(req.getPageNum(), req.getPageSize());
        IPage<SysMenuPO> page = TenantProvider.exec(() -> this.page(pageInfo, new LambdaQueryWrapper<SysMenuPO>()
                .eq(pid != null, SysMenuPO::getPid, pid)
                .eq(code != null, SysMenuPO::getCode, code)
                .likeRight(StringUtils.isNotBlank(tenant), SysMenuPO::getTenant, tenant)
                .like(StringUtils.isNotBlank(name), SysMenuPO::getName, name)
                .last("ORDER BY IFNULL(sort, " + Integer.MAX_VALUE + ") ASC, id DESC")
        ), CommonUtil.determineBizTenantOrSysTenant(JdContextUtil.currUserTenantNonNull(), tenantProperties.getBizTenantLevel()));
        List<SysMenuPO> poList = page.getRecords();
        
        // 查询租户信息
        Set<String> tenantSet = poList.stream()
                .filter(Objects::nonNull)
                .map(SysMenuPO::getTenant).collect(Collectors.toSet());
        Map<String, String> tenantAndNameMap = sysTenantService.mappingTenantName(tenantSet, true);
    
        // 组装数据
        List<SysMenuListRespVO> respList = poList.stream().map(po -> {
            SysMenuListRespVO resp = new SysMenuListRespVO();
            BeanUtils.copyProperties(po, resp);
            resp.setTenantName(tenantAndNameMap.get(resp.getTenant()));
            return resp;
        }).collect(Collectors.toList());
        return PageInfo.of(page.getTotal(), req.getPageNum(), req.getPageSize(), respList);
    }
    
    @Override
    public SysMenuDetailRespVO detail(Long menuId) {
        Objects.requireNonNull(menuId, "deptId cannot be null.");
        SysMenuPO po = getById(menuId);
        if (po == null) {
            return null;
        }
        return detail(po);
    }
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    @ValidTenant
    public SysMenuDetailRespVO update(SysMenuUpdateReqVO req) {
        String tenant = req.getTenant();
        Long id = req.getId();
        Long newPid = req.getPid();
        
        SysMenuPO dbPo = getById(id);
        if (dbPo == null) {
            throw new BaseException("role id -> " + id, BaseCodeMsgEnum.DATA_NON_EXIST_OR_NON_DATA_PERMISSION);
        }
        if (StringUtils.isBlank(tenant)) {
            tenant = dbPo.getTenant();
        }
        // 校验code是否被占用
        String code = req.getCode();
        if (code != null) {
            userCheckSupport.checkExistSameMenuCode(tenant, code, id);
        }
        SysMenuPO oldPo = new SysMenuPO();
        BeanUtils.copyProperties(dbPo, oldPo);
        Long oldPid = dbPo.getPid();
        
        SysMenuPO po = new SysMenuPO();
        BeanUtils.copyProperties(req, po, BaseConstant.TENANT);
        po.setTenant(tenant);
        // path处理
        if (newPid != null && !newPid.equals(oldPid)) {
            Assert.isTrue(!newPid.equals(id), "pid cannot equal id.");
            String newPath;
            if (newPid == 0) {
                newPath = id + "";
            } else {
                SysMenuPO parenMenu = getById(newPid);
                if (parenMenu == null) {
                    throw new BaseException("menu id -> " + newPid,
                            BaseCodeMsgEnum.DATA_NON_EXIST_OR_NON_DATA_PERMISSION);
                }
                boolean pidMenuIsSubMenu =
                        Arrays.stream(parenMenu.getPath().split(JdSymbolConstant.COMMA)).map(Long::parseLong).anyMatch(x -> Objects.equals(id, x));
                if (pidMenuIsSubMenu) {
                    throw new BaseException(BaseCodeMsgEnum.PARENT_MENU_CANNOT_BE_SUB_DEPT);
                }
                newPath = parenMenu.getPath() + "," + id;
            }
            
            po.setPath(newPath);
        }
        updateById(po);
        
        // 发布事件
        SysMenuPO lastPo = getById(id);
        applicationContext.publishEvent(new SysMenuUpdatedEvent(oldPo, lastPo));
        return detail(lastPo);
    }
    
    @Override
    @ValidTenant
    @Transactional(rollbackFor = Exception.class)
    public SysMenuDetailRespVO add(SysMenuAddReqVO req, boolean queryDetail) {
        Long pid = req.getPid();
        String code = req.getCode();
        String tenant = req.getTenant();
        if (StringUtils.isBlank(tenant)) {
            tenant = JdContextUtil.currUserTenantNonNull();
        }
        // 校验code是否被占用
        if (code != null) {
            userCheckSupport.checkExistSameGroupCode(tenant, code, null);
        }
        long id = IdWorker.getId();
        SysMenuPO po = new SysMenuPO();
        BeanUtils.copyProperties(req, po, BaseConstant.TENANT);
        po.setId(id);
        if (StringUtils.isNotBlank(tenant)) {
            po.setTenant(tenant);
        }
        if (pid == 0) {
            po.setPath(String.valueOf(id));
        } else {
            SysMenuDetailRespVO parentMenu = detail(pid);
            if (parentMenu == null) {
                throw new BaseException(BaseCodeMsgEnum.NON_EXIST_PARENT_MENU);
            }
            po.setPath(String.join(",", parentMenu.getPath(), String.valueOf(id)));
        }
        save(po);
        if (queryDetail) {
            return detail(id);
        }
        SysMenuDetailRespVO resp = new SysMenuDetailRespVO();
        resp.setId(po.getId());
        return resp;
    }
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void batchAdd(SysMenuBatchAddReqVO req) {
        String tenant = req.getTenant();
        List<SysMenuBatchAddMainReqVO> meneList = req.getMeneList();
        // 这里简单处理,  循环调用add方法即可
        for (SysMenuBatchAddMainReqVO menu : meneList) {
            if (StringUtils.isBlank(tenant)) {
                tenant = JdContextUtil.currUserTenantNonNull();
            }
            Long pid = menu.getPid();
            loopAddMenuItem(tenant, pid, menu);
        }
    }
    
    @Override
    @ValidId
    @Transactional(rollbackFor = Exception.class)
    public SysMenuDetailRespVO delete(Long menuId) {
        SysMenuPO po = getById(menuId);
        String path = po.getPath();
        
        boolean existChildMenu = getOne(new LambdaQueryWrapper<SysMenuPO>()
                .likeRight(SysMenuPO::getPath, path + JdSymbolConstant.COMMA)
                .last("limit 1")
        ) != null;
        if (existChildMenu) {
            throw new BaseException(BaseCodeMsgEnum.EXIST_SUB_DEPT);
        }
        
        SysMenuDetailRespVO resp = detail(po);
        // 删除
        removeById(menuId);
        // 发布事件
        applicationContext.publishEvent(new SysMenuDeletedEvent(po));
        return resp;
    }
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteByTenant(String tenant) {
        if (StringUtils.isNotBlank(tenant)) {
            SysValidator.verifyCouldUpdateThisTenant(tenant);
        } else {
            tenant = JdContextUtil.currUserTenantNonNull();
        }
        // 系统内置菜单不会删除
        Set<Long> systemMenuIdSet = list(new LambdaQueryWrapper<SysMenuPO>()
                .select(SysMenuPO::getId)
                .le(SysMenuPO::getId, BaseConstant.SYS_INTERNAL_DATA_ID_EQ)
                .likeRight(SysMenuPO::getTenant, tenant)
        ).stream()
                .map(SysMenuPO::getId)
                .collect(Collectors.toSet());
    
        List<SysMenuPO> deletedMenuList = list(new LambdaQueryWrapper<SysMenuPO>()
                .notIn(!CollectionUtils.isEmpty(systemMenuIdSet), SysMenuPO::getId, systemMenuIdSet)
                .likeRight(SysMenuPO::getTenant, tenant));
        
        remove(new LambdaQueryWrapper<SysMenuPO>()
                .notIn(!CollectionUtils.isEmpty(systemMenuIdSet), SysMenuPO::getId, systemMenuIdSet)
                .likeRight(SysMenuPO::getTenant, tenant));
        
        // 发布事件
        if (!CollectionUtils.isEmpty(deletedMenuList)) {
            applicationContext.publishEvent(new SysMenuBatchDeletedEvent(deletedMenuList));
        }
    }
    
    @NonNull
    @Override
    public Set<Long> loadMenuIdByRoleIds(@Nullable Set<Long> roleIdSet) {
        if (CollectionUtils.isEmpty(roleIdSet)) {
            return new HashSet<>();
        }
        // step1. 角色直接绑定的菜单id
        Set<Long> menuIdSet = sysRoleMenuReService.list(new LambdaQueryWrapper<SysRoleMenuRePO>()
                .select(SysRoleMenuRePO::getMenuId)
                .in(SysRoleMenuRePO::getRoleId, roleIdSet)
        )
                .stream()
                .filter(x -> x != null && x.getMenuId() != null)
                .map(SysRoleMenuRePO::getMenuId)
                .collect(Collectors.toSet());
        if (CollectionUtils.isEmpty(menuIdSet)) {
            log.info("menuIdSet is empty.");
            return new HashSet<>();
        }
        // step2. ant 匹配到用户 拥有的所有菜单
        List<SysMenuPO> allMenuList = sysMenuService.list(new LambdaQueryWrapper<SysMenuPO>()
                .select(SysMenuPO::getId, SysMenuPO::getPath)
        );
        List<SysMenuPO> hasDirectPermissionMenuList = allMenuList.stream()
                .filter(x -> menuIdSet.contains(x.getId()))
                .collect(Collectors.toList());
        Set<Long> hasPermissionMenuIdSet = new HashSet<>();
        for (SysMenuPO sysMenu : hasDirectPermissionMenuList) {
            for (SysMenuPO dbSysMenu : allMenuList) {
                if (antPathMatcher.match(sysMenu.getPath(), dbSysMenu.getPath())) {
                    hasPermissionMenuIdSet.add(dbSysMenu.getId());
                }
            }
        }
        return hasPermissionMenuIdSet;
    }
    
    @NotNull
    private SysMenuDetailRespVO detail(SysMenuPO po) {
        final SysMenuDetailRespVO resp = new SysMenuDetailRespVO();
        BeanUtils.copyProperties(po, resp);
        String tenant = po.getTenant();
        if (StringUtils.isNotBlank(tenant)) {
            Map<String, String> tenantAndNameMap = sysTenantService.mappingTenantName(Sets.newHashSet(tenant), true);
            resp.setTenant(tenantAndNameMap.get(tenant));
        }
        return resp;
    }
    
    /**
     * 递归增加菜单项
     *
     * @param tenant 租户
     * @param pid 父菜单id
     * @param item 菜单项
     */
    private void loopAddMenuItem(@NonNull String tenant, @NonNull Long pid, @NonNull SysMenuBatchAddItemReqVO item) {
        Integer type = item.getType();
        String code = item.getCode();
        String name = item.getName();
        Integer sort = item.getSort();
        String icon = item.getIcon();
        String component = item.getComponent();
        String componentName = item.getComponentName();
        Integer visible = item.getVisible();
        String remark = item.getRemark();
        
        SysMenuAddReqVO addReq = new SysMenuAddReqVO();
        addReq.setTenant(tenant);
        addReq.setPid(pid);
        addReq.setType(type);
        addReq.setCode(code);
        addReq.setName(name);
        addReq.setSort(sort);
        addReq.setIcon(icon);
        addReq.setComponent(component);
        addReq.setComponentName(componentName);
        addReq.setVisible(visible);
        addReq.setRemark(remark);
        pid = add(addReq, false).getId();
        List<SysMenuBatchAddItemReqVO> children = item.getChildren();
        if (CollectionUtils.isEmpty(children)) {
            return;
        }
        for (SysMenuBatchAddItemReqVO child : children) {
            loopAddMenuItem(tenant, pid, child);
        }
    }
}
